﻿using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using WeavingCommon;

namespace NotifyPropertyWeaverMsBuildTask
{
    [Export, PartCreationPolicy(CreationPolicy.Shared)]
    public class IlGeneratedByDependencyReader
    {
        TypeNodeBuilder typeNodeBuilder;

        [ImportingConstructor]
        public IlGeneratedByDependencyReader(TypeNodeBuilder typeNodeBuilder)
        {
            this.typeNodeBuilder = typeNodeBuilder;
        }

        public void Execute()
        {
            Process(typeNodeBuilder.NotifyNodes);
        }


        void Process(List<TypeNode> notifyNodes)
        {
            foreach (var node in notifyNodes)
            {
                Process(node);
                Process(node.Nodes);
            }
        }

        public static void Process(TypeNode node)
        {
            foreach (var property in node.TypeDefinition.Properties)
            {
                if (property.CustomAttributes.ContainsAttribute("DoNotNotifyAttribute"))
                {
                    return;
                }
                ProcessGet(property, node);
                ProcessSet(property, node);
            }
        }


        static void ProcessSet(PropertyDefinition property, TypeNode node)
        {
            var setMethod = property.SetMethod;

            //Exclude when no set
            if (setMethod == null)
            {
                return;
            }
            //Exclude when abstract
            if (setMethod.IsAbstract)
            {
                return;
            }
            foreach (var instruction in setMethod.Body.Instructions)
            {
                ProcessInstructionForSet(property, instruction, node);
            }
        }

        static void ProcessGet(PropertyDefinition property, TypeNode node)
        {
            var getMethod = property.GetMethod;

            //Exclude when no get
            if (getMethod == null)
            {
                return;
            }
            //Exclude when abstract
            if (getMethod.IsAbstract)
            {
                return;
            }
            foreach (var instruction in getMethod.Body.Instructions)
            {
                ProcessInstructionForGet(property, instruction, node);
            }
        }

        static void ProcessInstructionForSet(PropertyDefinition property, Instruction instruction, TypeNode node)
        {
            PropertyDefinition usedProperty;
            if (IsFieldSetInstruction(instruction, out usedProperty, node))
            {
                if (usedProperty == property)
                {
                    //skip where self reference
                    return;
                }
                node.PropertyDependencies.Add(new PropertyDependency
                                     {
                                         ShouldAlsoNotifyFor = usedProperty,
                                         WhenPropertyIsSet = property
                                     });
            }
        }

        public static bool IsFieldSetInstruction(Instruction instruction, out PropertyDefinition propertyDefinition, TypeNode node)
        {
            if ((instruction.OpCode.Code == Code.Stfld) && (instruction.Operand is FieldDefinition))
            {
                var fieldDefinition = (FieldDefinition)instruction.Operand;
                var mapping = node.Mappings.FirstOrDefault(x => x.FieldDefinition != null && x.FieldDefinition == fieldDefinition);
                if (mapping != null)
                {
                    propertyDefinition = mapping.PropertyDefinition;
                    return true;
                }
            }
            propertyDefinition = null;
            return false;
        }

        static void ProcessInstructionForGet(PropertyDefinition property, Instruction instruction, TypeNode node)
        {
            PropertyDefinition usedProperty;
            if (IsPropertyGetInstruction(instruction, out usedProperty, node) || IsFieldGetInstruction(instruction, out usedProperty, node))
            {
                if (usedProperty == property)
                {
                    //skip where self reference
                    return;
                }
                node.PropertyDependencies.Add(new PropertyDependency
                                     {
                                         ShouldAlsoNotifyFor = property,
                                         WhenPropertyIsSet = usedProperty
                                     });
            }
        }


        public static bool IsPropertyGetInstruction(Instruction instruction, out PropertyDefinition propertyDefinition, TypeNode node)
        {
            if (instruction.OpCode.IsCall() && (instruction.Operand is MethodReference))
            {
                var methodReference = (MethodReference)instruction.Operand;
                var mapping = node.Mappings.FirstOrDefault(x => x.PropertyDefinition.GetMethod == methodReference);
                if (mapping != null)
                {
                    propertyDefinition = mapping.PropertyDefinition;
                    return true;
                }
            }
            propertyDefinition = null;
            return false;
        }

        public static bool IsFieldGetInstruction(Instruction instruction, out PropertyDefinition propertyDefinition, TypeNode node)
        {
            if ((instruction.OpCode.Code == Code.Ldfld) && (instruction.Operand is FieldDefinition))
            {
                var fieldDefinition = (FieldDefinition)instruction.Operand;
                var mapping = node.Mappings.FirstOrDefault(x => x.FieldDefinition != null && x.FieldDefinition == fieldDefinition);
                if (mapping != null)
                {
                    propertyDefinition = mapping.PropertyDefinition;
                    return true;
                }
            }
            propertyDefinition = null;
            return false;
        }


    }
}